using System;
using System.Net;
using System.IO;
using System.Text;
using System.Net.Sockets;

namespace EmrFile
{
	public class MyFTP
	{
		private string  remoteHost,remotePath,remoteUser,remotePass,msg;
		/// <summary>
		/// Set the user password to use for logging into the remote server.
		/// </summary>
		public string RemotePass
		{
			get { return remotePass; }
			set { remotePass = value; }
		}
		/// <summary>
		/// Set the user name to use for logging into the remote server.
		/// 
		/// </summary>
		public string RemoteUser
		{
			get { return remoteUser; }
			set { remoteUser = value; }
		}
		/// <summary>
		/// Set the remote directory path.
		/// Return the current remote directory path.
		/// 
		/// </summary>
		public string RemotePath
		{
			get { return remotePath; }
			set { remotePath = value; }
		}
		/// <summary>
		/// Set the name of the FTP server to connect to.
		/// Return the name of the current FTP server.
		/// </summary>
		public string RemoteHost
		{
			get { return remoteHost; }
			set { remoteHost = value; }
		}

		private int remotePort,bytes;
		/// <summary>
		/// Set the port number to use for FTP.
		/// Return the current port number.
		/// </summary>
		public int RemotePort
		{
			get { return remotePort; }
			set { remotePort = value; }
		}
		private Socket clientSocket;

		private int retValue;
		private Boolean debug;
		private Boolean logined;
		private string reply;

		private static int BLOCK_SIZE = 512;

		Byte[] buffer = new Byte[BLOCK_SIZE];
		Encoding myEncoding=Encoding.Default;//Encoding.ASCII

		public MyFTP()
		{
			remoteHost  = "localhost";
			remotePath  = ".";
			remoteUser  = "anonymous";
			remotePass  = "anonymous";
			remotePort  = 21;
			debug     = false;
			logined    = false;
		}

		public string[] GetDirsList(string rPath)
		{
			string[] _FilesDirs=GetFileList("", rPath);
			string[] _fileDirs=new string[_FilesDirs.Length-1];
			int d=0;
			for (int i=0;i<_FilesDirs.Length;i++)
			{
				if (_FilesDirs[i].Length>0)
				{
					_fileDirs[d]=_FilesDirs[i];
					d+=1;
				}
			}
			d=0;
			string[] files=GetFilesList(rPath);
			string[] dirs=new string[_fileDirs.Length-files.Length];
			for (int i=0;i<_fileDirs.Length;i++)
			{
				if (Array.IndexOf(files, _fileDirs[i])<0)
				{
					dirs[d]=_fileDirs[i];
					d++;
				}
			}
			return dirs;
		}
		public string[] GetFilesList(string rPath)
		{
			ChangeDir(rPath);
			string[] _Files=GetFileList("*",rPath);
			string[] files=new string[_Files.Length-1];
			int d=0;
			for (int i=0;i<_Files.Length;i++)
			{
				if (_Files[i].Length>0)
				{
					files[d]=_Files[i];
					d+=1;
				}
			}
			return files;
		}

		/// <summary>
		/// Return a string array containing the remote directory's file list.
		/// </summary>
		/// <param name="mask"></param>
		/// <returns></returns>
		private string[] GetFileList(string mask ,string rPath)
		{
			if(!logined)
			{
				Connect();
			}
			ChangeDir(rPath);
			
			Socket cSocket = createDataSocket();
	
			sendCommand("NLST " + mask);
	
			if(!(retValue == 150 || retValue == 125))
			{
				throw new IOException(reply.Substring(4));
			}
	
			msg = "";
	
			while(true)
			{
				int bytes = cSocket.Receive(buffer, buffer.Length, 0);
				msg += myEncoding.GetString(buffer, 0, bytes);
	
				if(bytes < buffer.Length)
				{
					break;
				}
			}
	        //Edited by kyoui 20120512 
			//char[] seperator = {'\n'};
            char[] seperator = { '\n' };
            msg = msg.Replace("\r","");
			string[] mess = msg.Split(seperator);
			remotePath=".";
			cSocket.Close();
	
			readReply();
	
			if(retValue != 226)
			{
				throw new IOException(reply.Substring(4));
			}
			return mess;
		}

		/// <summary>
		/// Return the size of a file.
		/// </summary>
		/// <param name="fileName"></param>
		/// <returns></returns>
		public long GetFileSize(string fileName)
		{
			if(!logined)
			{
				Connect();
			}

			sendCommand("SIZE " + fileName);
			long size=0;

			if(retValue == 213)
			{
				size = Int64.Parse(reply.Substring(4));
			}
			else
			{
				throw new IOException(reply.Substring(4));
			}

			return size;
		}

		/// <summary>
		/// Login to the remote server.
		/// </summary>
		public void Connect()
		{
			clientSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
			IPEndPoint ep = new IPEndPoint(Dns.Resolve(remoteHost).AddressList[0], remotePort);

			try
			{
				clientSocket.Connect(ep);
			}
			catch(Exception)
			{
				throw new IOException("Couldn't connect to remote server");
			}

			readReply();
			if(retValue != 220)
			{
				DisConnect();
				throw new IOException(reply.Substring(4));
			}
			if(debug)
				Console.WriteLine("USER "+remoteUser);

			sendCommand("USER "+remoteUser);

			if(!(retValue == 331 || retValue == 230 ))
			{
				cleanup();
				throw new IOException(reply.Substring(4));
			}

			if( retValue != 230 )
			{
				if(debug)
					Console.WriteLine("PASS xxx");

				sendCommand("PASS "+remotePass);
				if(!(retValue == 230 || retValue == 202))
				{
					cleanup();
					throw new IOException(reply.Substring(4));
				}
			}

			logined = true;
			Console.WriteLine("Connected to "+remoteHost);

			ChangeDir(remotePath);
		}

		/// <summary>
		/// If the value of mode is true, set binary mode for downloads.
		/// Else, set Ascii mode.
		/// </summary>
		/// <param name="mode"></param>
		public void SetBinaryMode(Boolean mode)
		{
			if(mode)
			{
				sendCommand("TYPE I");
			}
			else
			{
				sendCommand("TYPE A");
			}
			if (retValue != 200)
			{
				throw new IOException(reply.Substring(4));
			}
		}

		/// <summary>
		/// Download a file to the Assembly's local directory,
		/// keeping the same file name.
		/// </summary>
		/// <param name="remFileName"></param>
		public void DownloadFile(string remFileName)
		{
			DownloadFile(remFileName,"",false);
		}

		/// <summary>
		/// Download a remote file to the Assembly's local directory,
		/// keeping the same file name, and set the resume flag.
		/// </summary>
		/// <param name="remFileName"></param>
		/// <param name="resume"></param>
		public void DownloadFile(string remFileName,Boolean resume)
		{
			DownloadFile(remFileName,"",resume);
		}

		/// <summary>
		/// Download a remote file to a local file name which can include
		/// a path. The local file name will be created or overwritten,
		/// but the path must exist.
		/// </summary>
		/// <param name="remFileName"></param>
		/// <param name="locFileName"></param>
		public void DownloadFile(string remFileName,string locFileName)
		{
			DownloadFile(remFileName,locFileName,false);
		}

		/// <summary>
		/// Download a remote file to a local file name which can include
		/// a path, and set the resume flag. The local file name will be
		/// created or overwritten, but the path must exist.
		/// </summary>
		/// <param name="remFileName"></param>
		/// <param name="locFileName"></param>
		/// <param name="resume"></param>
		public void DownloadFile(string remFileName,string locFileName,Boolean resume)
		{
			if(!logined)
			{
				Connect();
			}
	
			SetBinaryMode(true);
	
			Console.WriteLine("Downloading file "+remFileName+" from "+remoteHost + "/"+remotePath);
	
			if (locFileName.Equals(""))
			{
				locFileName = remFileName;
			}
	
			if(!File.Exists(locFileName))
			{
				Stream st = File.Create(locFileName);
				st.Close();
			}
	
			FileStream output = new FileStream(locFileName,FileMode.Open);
	
			Socket cSocket = createDataSocket();
	
			long offset = 0;
	
			if(resume)
			{
				offset = output.Length;
	
				if(offset > 0 )
				{
					sendCommand("REST "+offset);
					if(retValue != 350)
					{
						//throw new IOException(reply.Substring(4));
						//Some servers may not support resuming.
						offset = 0;
					}
				}
	
				if(offset > 0)
				{
					if(debug)
					{
						Console.WriteLine("seeking to " + offset);
					}
					long npos = output.Seek(offset,SeekOrigin.Begin);
					Console.WriteLine("new pos="+npos);
				}
			}
	
			sendCommand("RETR " + remFileName);
	
			if(!(retValue == 150 || retValue == 125))
			{
				throw new IOException(reply.Substring(4));
			}
	
			while(true)
			{
				bytes = cSocket.Receive(buffer, buffer.Length, 0);
				output.Write(buffer,0,bytes);
	
				if(bytes <= 0)
				{
					break;
				}
			}
	
			output.Close();
			if (cSocket.Connected)
			{
				cSocket.Close();
			}
	
			Console.WriteLine("");
	
			readReply();
	
			if(!(retValue == 226 || retValue == 250))
			{
				throw new IOException(reply.Substring(4));
			}
		}

		private void RemoteCopy(string remFileName, string remotePath)
		{
			bool resume=false;
			if(!logined)
			{
				Connect();
			}

			SetBinaryMode(true);

			Console.WriteLine("Copying file "+remFileName+" to " +remotePath);

			sendCommand("RETR " + remFileName);
			if(!(retValue == 150 || retValue == 125))
			{
				throw new IOException(reply.Substring(4));
			}
			//string fileName=RemotePath+remFileName.Substring(remFileName.LastIndexOf('/')+1);
			
			Socket cSocket = createDataSocket();
			while(true)
			{
				bytes = cSocket.Receive(buffer, buffer.Length, 0);
				//output.Write(buffer,0,bytes);

				if(bytes <= 0)
				{
					break;
				}
			}
		}

		/// <summary>
		/// Upload a file.
		/// </summary>
		/// <param name="fileName"></param>
		public void UploadFile(string fileName)
		{
			UploadFile(fileName,false);
		}

		/// <summary>
		/// Upload a file and set the resume flag.
		/// </summary>
		/// <param name="fileName"></param>
		/// <param name="resume"></param>
		public void UploadFile(string fileName,Boolean resume)
		{
			if(!logined)
			{
				Connect();
			}

			Socket cSocket = createDataSocket();
			long offset=0;

			if(resume)
			{
				try
				{
					SetBinaryMode(true);
					offset = GetFileSize(fileName);
				}
				catch(Exception)
				{
					offset = 0;
				}
			}

			if(offset > 0 )
			{
				sendCommand("REST " + offset);
				if(retValue != 350)
				{
					//throw new IOException(reply.Substring(4));
					//Remote server may not support resuming.
					offset = 0;
				}
			}

			sendCommand("STOR "+Path.GetFileName(fileName));

			if(!(retValue== 125 || retValue == 150))
			{
				throw new IOException(reply.Substring(4));
			}

			// open input stream to read source file
			FileStream input = new FileStream(fileName,FileMode.Open);

			if(offset != 0)
			{
				if(debug)
				{
					Console.WriteLine("seeking to " + offset);
				}
				input.Seek(offset,SeekOrigin.Begin);
			}

			Console.WriteLine("Uploading file "+fileName+" to "+remotePath);

			while ((bytes = input.Read(buffer,0,buffer.Length)) > 0)
			{

				cSocket.Send(buffer, bytes, 0);

			}
			input.Close();

			Console.WriteLine("");

			if (cSocket.Connected)
			{
				cSocket.Close();
			}

			readReply();
			if(!(retValue == 226 || retValue == 250))
			{
				throw new IOException(reply.Substring(4));
			}
		}

		/// <summary>
		/// Delete a file from the remote FTP server.
		/// </summary>
		/// <param name="fileName"></param>
		public void DeleteRemoteFile(string fileName)
		{
			if(!logined)
			{
				Connect();
			}

			sendCommand("DELE "+fileName);

			if(!(retValue == 550 || retValue == 250))
			{
				//550 ./emr/MR/01/00000001/kkk/test.xml: The system cannot find the file specified. \r
				throw new IOException(reply.Substring(4));
			}
		}

		/// <summary>
		/// Rename a file on the remote FTP server.
		/// </summary>
		/// <param name="oldFileName"></param>
		/// <param name="newFileName"></param>
		public void RenameRemoteFile(string oldFileName,string newFileName)
		{
			if(!logined)
			{
				Connect();
			}

			ChangeDir(oldFileName.Substring(0, oldFileName.LastIndexOf('/')+1));
			string ofName=oldFileName.Substring(oldFileName.LastIndexOf('/')+1);
			sendCommand("RNFR "+ofName);

			if(retValue != 350)//250
			{
				throw new IOException(reply.Substring(4));
			}
			//  known problem
			//  rnto will not take care of existing file.
			//  i.e. It will overwrite if newFileName exist
			string nfName=newFileName.Substring(newFileName.LastIndexOf('/')+1);
			sendCommand("RNTO "+nfName);
			if(retValue != 250)//350
			{
				throw new IOException(reply.Substring(4));
			}
		}

		/// <summary>
		/// Create a directory on the remote FTP server.
		/// </summary>
		/// <param name="dirName"></param>
		public bool CreateDir(string dirName)
		{
			if(!logined)
			{
				Connect();
			}

			sendCommand("MKD "+dirName);

			if(retValue == 257/*250*/)
			{//257 \"test\" directory created.\r
				return true;
			}
			else
			{//retValue == 550
				if (reply.Substring(4)==string.Format("{0}: Cannot create a file when that file already exists. \r",dirName))
				{//"550 test: Cannot create a file when that file already exists. \r"
					return true;
				}
				else if(reply.Substring(4)==string.Format("{0}: The system cannot find the path specified. \r",dirName))
				{//"550 ./emr/test0/test1: The system cannot find the path specified. \r"
					string[] dirs=dirName.Split('/');
					for (int i=0;i<dirs.Length;i++)
					{
						if(!ChangeDir(dirs[i]))
						{
							CreateDir(dirs[i]);
							ChangeDir(dirs[i]);
						}
					}
					return true;
				}
				else
				{
					throw new IOException(reply.Substring(4));
				}
			}
		}

		/// <summary>
		/// Delete a directory on the remote FTP server.
		/// </summary>
		/// <param name="dirName"></param>
		public void DeleteDir(string dirName)
		{
			if(!logined)
			{
				Connect();
			}
			string dir=dirName.Substring(0,dirName.Length-1);
			dir=dir.Substring(0,dir.LastIndexOf('/')+1);
			ChangeDir(dir);
			dir=dirName.Substring(0,dirName.Length-1);
			dir=dir.Substring(dir.LastIndexOf('/')+1);
			sendCommand("RMD "+dir);

			if(retValue != 250)
			{
				//"550 ./emr/test: The process cannot access the file because it is being used by another process. \r"
				//"550 ./emr/test: The system cannot find the file specified. \r"
				//"550 ./emr/test/: Access is denied. \r"
				//"550 ./emr/test: The directory is not empty. \r"
				throw new IOException(reply.Substring(4));
			}
		}

		/// <summary>
		/// Change the current working directory on the remote FTP server.
		/// </summary>
		/// <param name="dirName"></param>
		public bool ChangeDir(string dirName)
		{
			if(dirName.Equals("."))
			{
				//return ;
				return true;
			}

			if(!logined)
			{
				Connect();
			}

			sendCommand("CWD "+dirName);

			if(retValue == 250)
			{
				this.remotePath = dirName;
				Console.WriteLine("Current directory is "+remotePath);
				return true;
			}
			else
			{//"550 ./emr/test: The system cannot find the file specified. \r"
				//throw new IOException(reply.Substring(4));
				return false;
			}
		}

		/// <summary>
		/// Close the FTP connection.
		/// </summary>
		public void DisConnect()
		{
			if( clientSocket != null )
			{
				sendCommand("QUIT");
			}

			cleanup();
			Console.WriteLine("Closing...");
		}

		/// <summary>
		/// Set debug mode.
		/// </summary>
		/// <param name="debug"></param>
		private void setDebug(Boolean debug)
		{
			this.debug = debug;
		}

		/// <summary>
		/// 
		/// </summary>
		private void readReply()
		{
			msg = "";
			reply = readLine();
			retValue = Int32.Parse(reply.Substring(0,3));
		}

		/// <summary>
		/// 
		/// </summary>
		private void cleanup()
		{
			if(clientSocket!=null)
			{
				clientSocket.Close();
				clientSocket = null;
			}
			logined = false;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		private string readLine()
		{
			while(true)
			{
				bytes = clientSocket.Receive(buffer, buffer.Length, 0);
				msg +=myEncoding.GetString(buffer, 0, bytes);
				if(bytes < buffer.Length)
				{
					break;
				}
			}

			char[] seperator = {'\n'};
			string[] mess = msg.Split(seperator);

			if(msg.Length > 2)
			{
				msg = mess[mess.Length-2];
			}
			else
			{
				msg = mess[0];
			}

			if(!msg.Substring(3,1).Equals(" "))
			{
				return readLine();
			}

			if(debug)
			{
				for(int k=0;k < mess.Length-1;k++)
				{
					Console.WriteLine(mess[k]);
				}
			}
			return msg;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="command"></param>
		private void sendCommand(String command)
		{
			Byte[] cmdBytes = myEncoding.GetBytes((command+"\r\n").ToCharArray());
			clientSocket.Send(cmdBytes, cmdBytes.Length, 0);
			readReply();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		private Socket createDataSocket()
		{
			sendCommand("PASV");

			if(retValue != 227)
			{
				throw new IOException(reply.Substring(4));
			}

			int index1 = reply.IndexOf('(');
			int index2 = reply.IndexOf(')');
			string ipData = reply.Substring(index1+1,index2-index1-1);
			int[] parts = new int[6];

			int len = ipData.Length;
			int partCount = 0;
			string buf="";

			for (int i = 0; i < len && partCount <= 6; i++)
			{
				char ch = Char.Parse(ipData.Substring(i,1));
				if (Char.IsDigit(ch))
					buf+=ch;
				else if (ch != ',')
				{
					throw new IOException("Malformed PASV reply: " + reply);
				}

				if (ch == ',' || i+1 == len)
				{
					try
					{
						parts[partCount++] = Int32.Parse(buf);
						buf="";
					}
					catch (Exception)
					{
						throw new IOException("Malformed PASV reply: " + reply);
					}
				}
			}

			string ipAddress = parts[0] + "."+ parts[1]+ "." + parts[2] + "." + parts[3];

			int port = (parts[4] << 8) + parts[5];

			Socket s = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
			IPEndPoint ep = new IPEndPoint(Dns.Resolve(ipAddress).AddressList[0], port);

			try
			{
				s.Connect(ep);
			}
			catch(Exception)
			{
				throw new IOException("Can't connect to remote server");
			}

			return s;
		}
	}
}
